/*
 * Copyright 2011 Kim Lindhardt Madsen
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package dk.lindhardt.gwt.geie.client.evaluate;

import dk.lindhardt.gwt.geie.shared.*;
import org.matheclipse.parser.client.Parser;
import org.matheclipse.parser.client.SyntaxError;
import org.matheclipse.parser.client.ast.ASTNode;
import org.matheclipse.parser.client.eval.BooleanVariable;
import org.matheclipse.parser.client.eval.DoubleVariable;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;

/**
 * User: AnAmuser
 * Date: 07-08-11
 * <p/>
 * Calculates formula cells on a {@link TableLayout}
 */
@SuppressWarnings({"JavaDoc"})
public class GeieFormulaCalculator {

   private List<Cell> calculatableCells = new ArrayList<Cell>();
   private List<FormulaCell> currentlyCalculating = new ArrayList<FormulaCell>();
   private List<Cell> calculatedCells = new ArrayList<Cell>();
   private TableLayout layout;

   /**
    * A new FormulaCalculator. Uses a table layout to get the references in formulas.
    * @param layout the layout
    */
   public GeieFormulaCalculator(TableLayout layout) {
      this.layout = layout;
   }

   /**
    * Calculates all formula cells in the table layout. The calculated cells will be dirty after calculation.
    * Error cells that wrap formula cells is recalculated and if calculation succeeds
    * If some cells can't be calculated these cells are replaced with error cells which tries to describe what
    * went wrong.
    */
   public void calculateAllFormulas() {
      reset();
      boolean calculateNextRound = true;
      while (calculateNextRound) {
         calculateNextRound = false;
         calculatableCells.removeAll(calculatedCells);

         for (Cell cell : calculatableCells) {
            currentlyCalculating = new ArrayList<FormulaCell>();
            Serializable value;
            try {
               if (cell instanceof FormulaCell) {
                  FormulaCell formulaCell = (FormulaCell) cell;
                  value = calculateFormula(formulaCell);
                  if (value != null) {
                     Cell calcCell = createFormulaCell(value, formulaCell.getValue(), cell);
                     calcCell.setDirty(true);
                     calculatedCells.add(cell);
                     calculateNextRound = true;
                  }
               } else if (cell instanceof CellWrapper) {
                  CellWrapper cellWrapper = (CellWrapper) cell;
                  Cell wrappedCell = cellWrapper.getWrappedCell();
                  if (wrappedCell instanceof FormulaCell) {
                     FormulaCell formulaCell = (FormulaCell) wrappedCell;
                     value = calculateFormula(formulaCell);
                     if (value != null) {
                        Cell calcCell = createFormulaCell(value, formulaCell.getValue(), cell);
                        calcCell.setDirty(true);
                        calculatedCells.add(cell);
                        calculateNextRound = true;
                     }
                  }
               }
            } catch (GeieCalculationException e) {
               replaceWithErrorCell(cell, e.getMessage(), e.getCausingFormula());
            }
         }
      }
   }

   private Cell createFormulaCell(Serializable value, Formula formula, Cell oldCell) throws GeieCalculationException {
      //TODO remove to some kind of factory
      if (value instanceof Double) {
         NumberFormulaCell numberFormulaCell = new NumberFormulaCell(
               formula, oldCell.getPosition(), oldCell.getCellFormatKey());
         numberFormulaCell.setCalculatedValue((Double) value);
         layout.replaceCell(oldCell, numberFormulaCell);
         return numberFormulaCell;
      } else if (value instanceof Boolean) {
         BooleanFormulaCell booleanFormulaCell = new BooleanFormulaCell(
               formula, oldCell.getPosition(), oldCell.getCellFormatKey());
         booleanFormulaCell.setCalculatedValue((Boolean) value);
         layout.replaceCell(oldCell, booleanFormulaCell);
         return booleanFormulaCell;
      } else if (value instanceof Date) {
         Date dateValue = (Date) value;
         DateCell dateCell = new DateCell(dateValue, oldCell.getPosition(), oldCell.getCellFormatKey());
         layout.replaceCell(oldCell, dateCell);
         return dateCell;
      } else if (value instanceof String) {
         LabelCell labelCell = new LabelCell((String) value, oldCell.getPosition(), oldCell.getCellFormatKey());
         layout.replaceCell(oldCell, labelCell);
         return labelCell;
      }
      throw new RuntimeException("Evaluated value has no corresponding cell type. Value was of type " + value.getClass().getName());
   }

   /**
    * Calculates value of a single formula cell.
    * @param formulaCell the formula cell to be calculated
    * @return the calculated value
    * @throws dk.lindhardt.gwt.geie.shared.GeieCalculationException
    * <ul>
    *    <li>If there is cyclic references</li>
    *    <li>References to a cell that does not exist</li>
    *    <li>Syntax errors</li>
    *    <li>References to a non supported type</li>
    *    <li>References to a cell that made an error</li>
    * </ul>
    */
   protected Serializable calculateFormula(FormulaCell formulaCell) throws GeieCalculationException {
      Formula formula = formulaCell.getValue();
      if (currentlyCalculating.contains(formulaCell)) {
         throw new GeieCalculationException("This cell contains a cyclic reference.", formula);
      }
      currentlyCalculating.add(formulaCell);

      GeieEvaluator evaluator = new GeieEvaluator(new RangeBuilderImpl(formula));
      Map<String, Position> cellVariables = formula.getCellVariables();
      for (String variableName : cellVariables.keySet()) {
         Position position = cellVariables.get(variableName);
         Cell cell = layout.getCell(position.getRow(), position.getColumn());
         if (cell != null) {
            defineVariableFromCell(evaluator, variableName, cell);
         } else {
            throw new GeieCalculationException(
                  "The referenced cell position " + position.toString() + " does not exist", formula);
         }
      }

      try {
         Parser parser = new Parser(new GeieParserFactory(), true);
         ASTNode node = parser.parse(formula.getFormula());
         Serializable value = evaluator.evaluateNode(node);
         if (value != null) {
            currentlyCalculating.remove(formulaCell);
            calculatedCells.add(formulaCell);
         }
         return value;
      } catch (SyntaxError error) {
         throw new GeieCalculationException("Syntax error: " + error.getError(), error, formula);
      } catch (ArithmeticException e) {
         throw new GeieCalculationException(e.getMessage(), e, formula);
      } catch (IllegalArgumentException e) {
         throw new GeieCalculationException(e.getMessage(), e, formula);
      }
   }

   /**
    * Defines a variable with the given variable name and with the value from the cell
    */
   private void defineVariableFromCell(
         GeieEvaluator evaluator, String variableName, Cell cell) throws GeieCalculationException {
      if (cell instanceof CellWrapper) {
         defineVariableFromCell(evaluator, variableName, ((CellWrapper) cell).getWrappedCell());
      } else if (cell instanceof FormulaCell) {
         FormulaCell formulaCell = (FormulaCell) cell;
         try {
            Serializable value = getOrCalculateValue(formulaCell);
            if (value != null) {
               defineVariableFromValue(evaluator, variableName, value);
               Cell calcCell = createFormulaCell(value, formulaCell.getValue(), cell);
               calcCell.setDirty(true);
            }
         } catch (GeieCalculationException e) {
            throw new GeieCalculationException(
                  "There was an error at " + cell.getPosition() + "\r\nError was " + e.getMessage(), null);
         }
      } else {
         defineVariableFromValue(evaluator, variableName, cell.getValue());
      }
   }

   /**
    * Gets the calculated value of the cell and if not calculated yet it will be calculated.
    */
   private Serializable getOrCalculateValue(FormulaCell cell) throws GeieCalculationException {
      if (calculatedCells.contains(cell)) {
         return cell.getCalculatedValue();
      } else {
         return calculateFormula(cell);
      }
   }

   /**
    * Defines a variable with the given variable name and the given value
    */
   private void defineVariableFromValue(GeieEvaluator evaluator, String variableName, Serializable value) {
      if (value instanceof Double) {
         DoubleVariable variable = new DoubleVariable((Double) value);
         evaluator.defineVariable(variableName, variable);
      } else if (value instanceof Boolean) {
         BooleanVariable variable = new BooleanVariable((Boolean) value);
         evaluator.defineVariable(variableName, variable);
      } else if (value instanceof Date) {
         DateVariable variable = new DateVariable((Date) value);
         evaluator.defineVariable(variableName, variable);
      } else if (value instanceof String) {
         StringVariable variable = new StringVariable((String) value);
         evaluator.defineVariable(variableName, variable);
      } else if (value instanceof TZIgnoringDate) {
         DateVariable variable = new DateVariable(((TZIgnoringDate) value).getDate());
         evaluator.defineVariable(variableName, variable);
      }
   }


   /**
    * Wrap the original cell in a error cell
    */
   private void replaceWithErrorCell(Cell originalCell, String errorMessage, Formula causingFormula) {
      ErrorCell errorCell = new ErrorCell(causingFormula, errorMessage, originalCell);
      errorCell.setDirty(true);
      layout.replaceCell(originalCell, errorCell);
   }

   /**
    * Resets this formulaHandler. Should be called between independent calculations.
    */
   protected void reset() {
      currentlyCalculating = new ArrayList<FormulaCell>();
      calculatedCells = new ArrayList<Cell>();
      calculatableCells = new ArrayList<Cell>();

      List<Cell> cells = layout.getCells();
      for (Cell cell : cells) {
         if (cell instanceof FormulaCell) {
            calculatableCells.add(cell);
         } else if (cell instanceof CellWrapper) {
            Cell wrappedCell = ((CellWrapper) cell).getWrappedCell();
            if (wrappedCell instanceof FormulaCell) {
               calculatableCells.add(cell);
            }
         }
      }
   }

   protected class RangeBuilderImpl implements GeieEvaluator.RangeBuilder {

      private Formula formula;

      public RangeBuilderImpl(Formula formula) {
         this.formula = formula;
      }

      public Range buildRange(String left, String right) {
         Map<String, Position> cellVariables = formula.getCellVariables();
         Position leftPosition = cellVariables.get(left);
         Position rightPosition = cellVariables.get(right);
         int highRow = rightPosition.getRow();
         int lowRow = leftPosition.getRow();
         int highCol = rightPosition.getColumn();
         int lowCol = leftPosition.getColumn();

         List<Cell> cells = layout.getRange(
               new Position(highRow, highCol), new Position(lowRow, lowCol), PositioningType.EXCEL);

         Range range = new Range();
         for (Cell cell : cells) {
            Position position = cell.getPosition();
            int row = position.getRow();
            int column = position.getColumn();
            if (cell instanceof FormulaCell) {
               try {
                  range.setValue(row, column, getOrCalculateValue((FormulaCell) cell));
               } catch (GeieCalculationException e) {
                  throw new IllegalArgumentException(e.getMessage());
               }
            } else {
               range.setValue(row, column, cell.getValue());
            }
         }

         return range;
      }
   }
}
